package com.zy.explore.core;

import org.springframework.util.Assert;

import java.io.Serializable;
import java.nio.ByteBuffer;
import java.time.Instant;

/**
 * description 实现了 MongoDB 的 ObjectId 的逻辑
 *
 * @author ZY
 * @date 2021-05-05 18:04
 */
public final class ObjectId implements Comparable<ObjectId>, Serializable {

    private static final long serialVersionUID = 198415617L;

    private static final char[] HEX_CHARS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    private final int timestamp;

    private final int machineId;

    private final short processId;

    private final int idCounter;

    public ObjectId(int timestamp, int machineId, short processId, int idCounter) {
        Assert.isTrue((machineId & 0xff000000) == 0, "machineId must fit in three bytes");
        Assert.isTrue((idCounter & 0xff000000) == 0, "idCounter must fit in three bytes");

        this.timestamp = timestamp;
        this.machineId = machineId;
        this.processId = processId;
        this.idCounter = idCounter;
    }

    public byte[] toByteArray() {
        return toByteBuffer().array();
    }

    public ByteBuffer toByteBuffer() {
        ByteBuffer buffer = ByteBuffer.allocate(12);

        toByteBuffer(buffer);
        buffer.flip();

        return buffer;
    }

    public void toByteBuffer(ByteBuffer buffer) {
        Assert.notNull(buffer, "buffer must not be null");
        Assert.isTrue(buffer.remaining() >= 12, "buffer.remaining() must be greater than 11");

        buffer.put(int3(this.timestamp));
        buffer.put(int2(this.timestamp));
        buffer.put(int1(this.timestamp));
        buffer.put(int0(this.timestamp));
        buffer.put(int2(this.machineId));
        buffer.put(int1(this.machineId));
        buffer.put(int0(this.machineId));
        buffer.put(short1(this.processId));
        buffer.put(short0(this.processId));
        buffer.put(int2(this.idCounter));
        buffer.put(int1(this.idCounter));
        buffer.put(int0(this.idCounter));
    }

    public long getTimestamp() {
        return ((long) timestamp) & 0xffffffffL;
    }

    public Instant getInstant() {
        return Instant.ofEpochSecond(getTimestamp());
    }

    public int getMachineId() {
        return machineId;
    }

    public int getProcessId() {
        return ((int) processId) & 0xffff;
    }

    public int getIdCounter() {
        return idCounter;
    }

    public String toHexString() {
        char[] chars = new char[24];
        int i = 0;
        byte[] bytes = this.toByteArray();

        for (byte b : bytes) {
            chars[i++] = HEX_CHARS[b >> 4 & 15];
            chars[i++] = HEX_CHARS[b & 15];
        }

        return new String(chars);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        ObjectId objectId = (ObjectId) o;

        return timestamp == objectId.timestamp && idCounter == objectId.idCounter &&
            machineId == objectId.machineId && processId == objectId.processId;
    }

    @Override
    public int hashCode() {
        int result = timestamp;
        result = 31 * result + machineId;
        result = 31 * result + (int) processId;
        return 31 * result + idCounter;
    }

    @Override
    public int compareTo(ObjectId o) {
        if (o == null) {
            throw new NullPointerException("o must not be null");
        }

        if (timestamp != o.timestamp) {
            return Long.compare(getTimestamp(), o.getTimestamp());
        } else if (machineId != o.machineId) {
            return Integer.compare(machineId, o.machineId);
        } else if (processId != o.processId) {
            return Integer.compare(getProcessId(), o.getProcessId());
        } else {
            return Integer.compare(idCounter, o.idCounter);
        }
    }

    @Override
    public String toString() {
        return toHexString();
    }

    public static ObjectId fromByteArray(byte[] bytes) {
        Assert.notNull(bytes, "bytes must not be null");
        Assert.isTrue(bytes.length == 12, "bytes length must be 12");

        return readBuffer(ByteBuffer.wrap(bytes));
    }

    public static ObjectId fromByteBuffer(ByteBuffer buffer) {
        Assert.notNull(buffer, "buffer must not be null");
        Assert.isTrue(buffer.remaining() >= 12, "buffer.remaining() must be greater than 11");

        return readBuffer(buffer);
    }

    public static ObjectId fromHexString(String hexString) {
        Assert.isTrue(isValidObjectId(hexString), "hexString must be an ObjectId");

        byte[] bytes = new byte[12];

        for (int i = 0; i < bytes.length; ++i) {
            int value = (hexValue(hexString.charAt(i << 1)) << 4) | hexValue(hexString.charAt((i << 1) | 1));
            bytes[i] = (byte) (value & 0xFF);
        }

        return readBuffer(ByteBuffer.wrap(bytes));
    }

    public static boolean isValidObjectId(String hexString) {
        Assert.notNull(hexString, "hexString must not be null");

        int len = hexString.length();
        if (len != 24) {
            return false;
        }

        for (int i = 0; i < len; ++i) {
            char c = hexString.charAt(i);
            if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) {
                return false;
            }
        }

        return true;
    }

    private static ObjectId readBuffer(ByteBuffer buffer) {
        int timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get());
        int randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());
        short randomValue2 = makeShort(buffer.get(), buffer.get());
        int counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());

        return new ObjectId(timestamp, randomValue1, randomValue2, counter);
    }

    private static int hexValue(char hex) {
        if (hex >= '0' && hex <= '9') {
            return hex - '0';
        } else if (hex >= 'a' && hex <= 'f') {
            return (hex - 'a') + 10;
        } else {
            return (hex - 'A') + 10;
        }
    }

    private static int makeInt(byte b3, byte b2, byte b1, byte b0) {
        return b3 << 24 | (b2 & 255) << 16 | (b1 & 255) << 8 | b0 & 255;
    }

    private static short makeShort(byte b1, byte b0) {
        return (short) ((b1 & 255) << 8 | b0 & 255);
    }

    private static byte int3(int x) {
        return (byte) ((x >>> 24) & 0xFF);
    }

    private static byte int2(int x) {
        return (byte) ((x >>> 16) & 0xFF);
    }

    private static byte int1(int x) {
        return (byte) ((x >>> 8) & 0xFF);
    }

    private static byte int0(int x) {
        return (byte) (x & 0xFF);
    }

    private static byte short1(short x) {
        return (byte) ((x >>> 8) & 0xFF);
    }

    private static byte short0(short x) {
        return (byte) (x & 0xFF);
    }
}
